import os
import chardet
import time
import docx
import re
import json
import uuid
import threading
from typing import List, Dict, Tuple
from flask import Flask, request, jsonify
from openai import OpenAI
from collections import Counter
from flask_cors import CORS

app = Flask(__name__)
# 配置CORS - 允许所有来源
CORS(app, supports_credentials=True, origins=["*"])


# 使用线程安全的全局变量管理器
class ExamBotManager:
    def __init__(self):
        self.lock = threading.Lock()
        self.bots = {}

    def get_bot(self, session_id):
        with self.lock:
            return self.bots.get(session_id)

    def create_bot(self, session_id, config):
        with self.lock:
            bot = SmartExamBot(
                api_key=config["api_key"],
                base_url=config["base_url"],
                max_questions=config["max_questions"]
            )
            self.bots[session_id] = bot
            return bot


exam_bot_manager = ExamBotManager()


class SmartExamBot:
    def __init__(self, api_key: str, base_url: str, max_questions: int = 5):
        """初始化考试机器人"""
        self.api_key = api_key
        self.base_url = base_url
        self.client = OpenAI(
            api_key=api_key,
            base_url=base_url
        )
        self.question_count = 0
        self.total_score = 0
        self.max_possible_score = 0
        self.max_questions = max_questions
        self.question_scores = []  # 记录每题得分
        self.cannot_answer_patterns = re.compile(
            r'不知道|不清楚|没学过|不懂|不了解|不会做|不知道怎么答|无法回答|我举不出来例子|分析不出来',
            re.IGNORECASE
        )
        self.irrelevant_patterns = re.compile(
            r'跑题|偏题|无关|答非所问|换个题|另一个|不相关|说点别的|其他内容|别的话题',
            re.IGNORECASE
        )
        self.image_patterns = re.compile(
            r'如图|图示|图片|图表|图[.\d]+|figure|graph|chart|diagram|illustration',
            re.IGNORECASE
        )
        self.knowledge_content = ""  # 存储知识库内容
        self.keywords = []  # 存储知识库关键词
        self.current_question = ""  # 存储当前题目内容
        self.question_dimensions = []  # 存储题目要求的维度
        self.messages = []  # 存储对话历史
        # 扩展乱答检测的无关词汇库
        self.unrelated_topics = {'天气', '吃饭', '电影', '游戏', '睡觉', 'time', 'date',
                                 'music', 'song', 'star', 'movie', 'travel', 'shopping', 'food',
                                 'sports', 'exercise', 'novel', 'book', 'TV', 'variety'}

    def _extract_keywords(self, text: str) -> List[str]:
        """从知识库中提取关键词"""
        words = re.findall(r'\b[\u4e00-\u9fa5]{2,6}\b', text)
        word_counts = Counter(words)

        stop_words = {'可以', '一个', '这个', '我们', '进行', '需要', '应该', '问题', '方法'}
        keywords = [word for word, count in word_counts.most_common(20)
                    if word not in stop_words and count > 1]

        return keywords[:10] if keywords else ["数据", "分析", "技术"]

    def _extract_dimensions(self, question: str) -> List[str]:
        """提取题目要求的分析维度"""
        match = re.search(r'请从(.*?)分析', question)
        if match:
            dimensions_text = match.group(1)
            dimensions = [dim.strip() for dim in re.split(r'、|和|与', dimensions_text) if dim.strip()]
            return dimensions
        return []

    def detect_encoding(self, file_path: str) -> str:
        """检测文件编码"""
        if file_path.lower().endswith('.docx'):
            return 'docx'
        try:
            with open(file_path, 'rb') as file:
                result = chardet.detect(file.read(10240))
                return result['encoding'] or 'utf-8'
        except Exception as e:
            raise Exception(f"编码检测失败: {str(e)}")

    def read_file_content(self, file_path: str) -> str:
        """读取文件内容，支持txt和docx格式"""
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"文件不存在: {file_path}")

        if not os.path.isfile(file_path):
            raise IsADirectoryError(f"{file_path}不是一个文件")

        try:
            encoding = self.detect_encoding(file_path)

            if encoding == 'docx':
                doc = docx.Document(file_path)
                full_text = []

                for para in doc.paragraphs:
                    full_text.append(para.text)

                for table in doc.tables:
                    for row in table.rows:
                        row_text = [cell.text for cell in row.cells]
                        full_text.append('\t'.join(row_text))

                return '\n'.join(full_text)

            encodings_to_try = [encoding, 'utf-8', 'gbk', 'latin-1']
            for enc in encodings_to_try:
                try:
                    with open(file_path, 'r', encoding=enc, errors='replace') as file:
                        return file.read()
                except UnicodeDecodeError:
                    continue
            raise Exception("无法识别文件编码")
        except Exception as e:
            raise Exception(f"文件读取失败: {str(e)}")

    def get_ai_response(self, messages: List[Dict], max_retries: int = 3) -> str:
        """获取AI响应，带重试机制"""
        retry_count = 0
        while retry_count < max_retries:
            try:
                response = self.client.chat.completions.create(
                    model="deepseek-v3",
                    messages=messages,
                    temperature=0.2,
                    timeout=30
                )
                return response.choices[0].message.content
            except Exception as e:
                retry_count += 1
                if retry_count >= max_retries:
                    raise Exception(f"API请求失败: {str(e)}")
                print(f"API调用失败，正在重试({retry_count}/{max_retries})...")
                time.sleep(2)

    def is_cannot_answer(self, user_input: str) -> bool:
        """判断用户输入是否表示'不会'"""
        user_input = user_input.strip()
        if not user_input or re.fullmatch(r'[\s,.!?;]+', user_input):
            return True

        return self.cannot_answer_patterns.search(user_input) is not None

    def is_irrelevant(self, user_answer: str) -> Tuple[bool, str]:
        """判断回答是否与题目和知识库无关（乱答判定）"""
        user_answer = user_answer.strip().lower()

        if self.irrelevant_patterns.search(user_answer):
            return True, "回答明确表示与题目无关"

        question_keywords = re.findall(r'\b[\u4e00-\u9fa5]{2,6}\b', self.current_question)
        all_keywords = question_keywords + self.keywords + self.question_dimensions

        has_relevant_keywords = any(keyword in user_answer for keyword in all_keywords)

        if not has_relevant_keywords:
            for topic in self.unrelated_topics:
                if topic in user_answer:
                    return True, f"回答涉及无关主题({topic})，视为乱答"

            if len(user_answer) < 5:
                return True, "回答过短且不涉及题目相关内容，视为乱答"

            return True, "回答与题目及知识库内容无关，视为乱答"

        return False, ""

    def _get_system_prompt(self):
        """生成系统提示"""
        return f"""
        你是一个公平的智能考试机器人，需要基于以下知识库内容出题和评分：
        {self.knowledge_content[:2000]}...

        工作模式：

        1. 题目生成模式：
        - 必须从知识库中提取具体概念出题
        - 题目必须包含至少1个关键词：{', '.join(self.keywords)}
        - 必须要求从2-3个维度回答
        - 示例："请从技术原理和应用场景两个维度分析知识库中提到的[关键词]"
        - 只返回题目，不要包含评分信息
        - 绝对不能包含任何图片、图示相关的内容

        2. 评分模式：
        - 评分标准（宽松版）：
          10分：全面覆盖所有维度，准确引用知识库内容，有具体实例
          8-9分：覆盖主要维度，正确引用知识库内容，有一定深度
          6-7分：部分覆盖维度，与知识库有一定关联，内容较简单
          4-5分：提到基本概念，与题目相关但深度不足，或关联较弱
          0分：未涉及题目核心内容或乱答

        - 反馈必须指出：
          * 回答覆盖了哪些维度
          * 与知识库内容的关联程度
          * 具体改进建议
        - 评分原则：只要与题目相关且有一定道理，就应给予合理分数
        """

    def init_exam(self, knowledge_content: str):
        """初始化考试"""
        self.knowledge_content = knowledge_content
        self.keywords = self._extract_keywords(knowledge_content)

        self.messages = [
            {"role": "system", "content": self._get_system_prompt()},
            {"role": "assistant", "content": "考试准备就绪，请输入'开始'进行第一题。"}
        ]

    def generate_question(self):
        """题目生成逻辑"""
        if self.question_count >= self.max_questions:
            return {"message": "考试已完成所有题目"}

        self.question_count += 1
        max_retries = 5
        retry_count = 0

        while retry_count < max_retries:
            print(f"\n===== 第{self.question_count}/{self.max_questions}题 =====")

            prompt = f"""请基于以下知识库内容出一道主观题：
            {self.knowledge_content[:3000]}

            要求：
            1. 必须从以下关键词中选择至少1个：{', '.join(self.keywords)}
            2. 题目形式应为：
               "请从[维度1]、[维度2]和[维度3]分析知识库中提到的[关键词]..."
            3. 不要包含示例和解释
            4. 确保题目在知识库中有明确答案依据
            5. 绝对不能包含任何图片、图示、图表相关的内容
               例如：不能出现"如图所示"、"根据图X"、"figure"等表述"""

            self.messages.append({"role": "user", "content": prompt})
            question = self.get_ai_response(self.messages)

            if not any(keyword in question for keyword in self.keywords):
                print("⚠️ 题目未包含关键词，重新生成...")
                retry_count += 1
                self.messages.pop()
                continue

            if len(re.findall(r'维度|角度|方面', question)) < 1:
                print("⚠️ 题目未明确要求多维度回答，重新生成...")
                retry_count += 1
                self.messages.pop()
                continue

            if self.image_patterns.search(question):
                print("⚠️ 题目包含图片相关表述，重新生成...")
                retry_count += 1
                self.messages.pop()
                continue

            question = question.split("|")[0].strip()
            question = re.sub(r'^\d+[\.\d]*\s*', '', question)

            if not question or len(question) < 10:
                print("⚠️ 题目过短，重新生成...")
                retry_count += 1
                self.messages.pop()
                continue

            self.current_question = question
            self.question_dimensions = self._extract_dimensions(question)
            self.messages.append({"role": "assistant", "content": question})
            self.max_possible_score += 10
            return question
        else:
            print("⚠️ 多次尝试后仍无法获取有效题目，跳过本题")
            self.question_count -= 1
            return None

    def evaluate_answer(self, user_answer: str):
        """评估用户答案"""
        if self.is_cannot_answer(user_answer):
            self.total_score += 0
            self.question_scores.append(0)
            self.messages.append({"role": "user", "content": user_answer})
            self.messages.append({"role": "assistant", "content": "0|检测到无法回答的表述|否"})
            return {
                "score": 0,
                "feedback": "检测到无法回答的表述，本题得0分",
                "need_follow_up": False
            }

        is_irrelevant, reason = self.is_irrelevant(user_answer)
        if is_irrelevant:
            self.total_score += 0
            self.question_scores.append(0)
            self.messages.append({"role": "user", "content": user_answer})
            self.messages.append({"role": "assistant", "content": f"0|{reason}|否"})
            return {
                "score": 0,
                "feedback": f"检测到无关回答: {reason}，本题得0分",
                "need_follow_up": False
            }

        self.messages.append({"role": "user", "content": user_answer})

        evaluation = self.get_ai_response(self.messages + [
            {"role": "system", "content": f"""请按以下宽松标准评分：
            1. 知识库关键内容：{self.knowledge_content[:1000]}
            2. 题目要求维度：{', '.join(self.question_dimensions)}
            3. 评分标准：
               10分：全面覆盖所有维度，准确引用知识库内容，有具体实例
               8-9分：覆盖主要维度，正确引用知识库内容，有一定深度
               6-7分：部分覆盖维度，与知识库有一定关联，内容较简单
               4-5分：提到基本概念，与题目相关但深度不足，或关联较弱
               0分：未涉及题目核心内容或乱答

            5. 反馈必须包含：
               - 回答覆盖了哪些维度
               - 与知识库内容的关联点（具体到关键词或概念）
               - 具体改进建议
            6. 评分原则：只要与题目相关且有一定道理，就应给予合理分数，不要过于严格
            """},
        ])

        if not re.fullmatch(r'^\d+\|.+\|(是|否)$', evaluation.strip()):
            print("\n⚠️ 评分格式错误，使用默认评分")
            evaluation = "6|评分系统错误，按中等分数处理|否"

        try:
            score_part, feedback, need_follow_up = evaluation.strip().split('|')
            score = max(0, min(10, float(score_part)))

            question_keywords = re.findall(r'\b[\u4e00-\u9fa5]{2,6}\b', self.current_question)
            has_question_keywords = any(keyword in user_answer for keyword in question_keywords)
            has_knowledge_keywords = any(keyword in user_answer for keyword in self.keywords)

            if score < 4 and (has_question_keywords or has_knowledge_keywords):
                score = 4
                feedback = f"回答涉及题目关键词，调整分数。{feedback}"

            self.total_score += score
            self.question_scores.append(score)

            self.messages.append(
                {"role": "assistant", "content": f"{score}|{feedback}|否"})

            return {
                "score": score,
                "feedback": feedback,
                "need_follow_up": False
            }
        except Exception as e:
            print(f"\n⚠️ 评分解析错误: {str(e)}")
            self.total_score += 6
            self.question_scores.append(6)
            return {
                "score": 6,
                "feedback": "评分解析错误，按中等分数处理",
                "need_follow_up": False
            }


@app.route('/')
def index():
    """前端页面"""
    with open('test.html', 'r', encoding='utf-8') as f:
        return f.read()


@app.route('/start_exam', methods=['POST'])
def start_exam():
    """开始考试并获取第一题"""
    session_id = request.cookies.get('session_id') or str(uuid.uuid4())

    CONFIG = {
        "api_key": "sk-43f6c93de4ff47ba9cbd7e0dc9ed68b7",
        "base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1",
        "file_path": r"C:\Users\dell\Desktop/第3章 数据获取.docx",
        "max_questions": 5
    }

    bot = exam_bot_manager.get_bot(session_id)
    if not bot:
        bot = exam_bot_manager.create_bot(session_id, CONFIG)

    try:
        knowledge_content = bot.read_file_content(CONFIG["file_path"])
        bot.init_exam(knowledge_content)

        question = bot.generate_question()

        if question:
            response = jsonify({
                'question': question,
                'question_count': bot.question_count,
                'max_questions': bot.max_questions,
                'keywords': bot.keywords
            })
            response.set_cookie('session_id', session_id, httponly=True, samesite='None', secure=True)
            return response
        else:
            return jsonify({'error': '题目生成失败'}), 500
    except Exception as e:
        return jsonify({'error': f'考试启动失败: {str(e)}'}), 500


@app.route('/submit_answer', methods=['POST'])
def submit_answer():
    """提交答案并获取评分"""
    session_id = request.cookies.get('session_id')
    if not session_id:
        return jsonify({'error': '缺少会话ID'}), 400

    bot = exam_bot_manager.get_bot(session_id)
    if not bot:
        return jsonify({'error': '考试未开始或会话已过期'}), 400

    data = request.json
    user_answer = data.get('answer', '')
    username = data.get('username', '')

    # 评估答案
    result = bot.evaluate_answer(user_answer)

    # 返回结果
    result['question_count'] = bot.question_count
    result['current_question'] = bot.question_count
    result['total_score'] = bot.total_score
    result['max_possible_score'] = bot.max_possible_score
    result['username'] = username

    return jsonify(result)


@app.route('/next_question', methods=['GET'])
def next_question():
    """获取下一题"""
    session_id = request.cookies.get('session_id')
    if not session_id:
        return jsonify({'error': '缺少会话ID'}), 400

    bot = exam_bot_manager.get_bot(session_id)
    if not bot:
        return jsonify({'error': '考试未开始或会话已过期'}), 400

    try:
        if bot.question_count < bot.max_questions:
            question = bot.generate_question()
            if question:
                return jsonify({
                    'question': question,
                    'question_count': bot.question_count,
                    'current_question': bot.question_count
                })
            else:
                return jsonify({'error': '题目生成失败'}), 500
        else:
            return jsonify({
                'message': '考试已完成所有题目',
                'total_score': bot.total_score,
                'max_possible_score': bot.max_possible_score,
                'average_score': bot.total_score / bot.max_questions,
                'question_scores': bot.question_scores
            })
    except Exception as e:
        return jsonify({'error': f'获取题目失败: {str(e)}'}), 500


@app.route('/request_human_intervention', methods=['POST'])
def request_human_intervention():
    """处理人工介入请求"""
    session_id = request.cookies.get('session_id')
    if not session_id:
        return jsonify({'error': '缺少会话ID'}), 400

    bot = exam_bot_manager.get_bot(session_id)
    if not bot:
        return jsonify({'error': '考试未开始或会话已过期'}), 400

    context = {
        "current_question": bot.current_question,
        "question_history": [msg["content"] for msg in bot.messages if msg["role"] == "assistant"],
        "answer_history": [msg["content"] for msg in bot.messages if msg["role"] == "user"],
        "scores": bot.question_scores
    }

    return jsonify({
        'message': '人工介入请求已提交，请等待客服联系',
        'request_id': str(uuid.uuid4()),
        'context': context
    })


if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=True)